package fr.upmc.alasca.centreCalcul.interne.repartiteurRequetes;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;

import fr.upmc.alasca.centreCalcul.interne.controleurApplication.connectors.ControleurApplicationRepartitionRequeteConnector;
import fr.upmc.alasca.centreCalcul.interne.controleurApplication.interfaces.ControleurApplicationRepartitionRequeteI;
import fr.upmc.alasca.centreCalcul.interne.controleurApplication.ports.ControleurApplicationRepartitionRequeteOutboundPort;
import fr.upmc.alasca.centreCalcul.interne.machinePhysique.machineVirtuelle.connectors.MachineVirtuelleApplicationConnector;
import fr.upmc.alasca.centreCalcul.interne.machinePhysique.machineVirtuelle.interfaces.MachineVirtuelleApplicationI;
import fr.upmc.alasca.centreCalcul.interne.machinePhysique.machineVirtuelle.ports.MachineVirtuelleApplicationOutboundPort;
import fr.upmc.alasca.centreCalcul.interne.repartiteurRequetes.interfaces.RepartiteurRequetesClientI;
import fr.upmc.alasca.centreCalcul.interne.repartiteurRequetes.ports.RepartiteurRequetesClientInboundPort;
import fr.upmc.alasca.centreCalcul.objets.Delais;
import fr.upmc.alasca.centreCalcul.objets.Requete;
import fr.upmc.alasca.centreCalcul.outils.LogMessage;
import fr.upmc.components.AbstractComponent;
import fr.upmc.components.exceptions.ComponentStartException;


/**
 * @author Antoine Lavaire, Sabri Labassi, Thibaut Fleury, Ryme Fantazi
 */
public	class	RepartiteurRequetes
extends			AbstractComponent
implements		RepartiteurRequetesClientI
{
	/* ------------------------------------------------------------- ATTRIBUT */
	protected	String													portUri; // = uriApplication
	protected	Boolean													estDistribue;

	// Compteur de requêtes reçues
	protected	long													nbRequetesRecues;
	// Association (URI -> port) des machines virtuelles utilisées
	protected	Map<String, MachineVirtuelleApplicationOutboundPort>	machinesVirtuelles;
	// Associations (machine virtuelle -> temps moyen d'exécution des requêtes)
	protected	Map<String, Long>										tempsExecution;
	
	// Port de connexion (outbound) vers le controleur d'application
	protected	ControleurApplicationRepartitionRequeteOutboundPort		controleurApplication;
	protected	Delais													tempsRafraichissement;




	/* --------------------------------------------------------- CONSTRUCTEUR */
	public	RepartiteurRequetes
	(
		String	p_portUri,
		Boolean	p_estDistribue,
		String	p_uriControleur,
		Delais	p_tempsRafraichissement
	) throws	Exception
	{
		super(true, true);

		portUri = p_portUri;
		estDistribue = p_estDistribue;
		tempsRafraichissement = p_tempsRafraichissement;
		tempsExecution = new HashMap<>();
		
		LogMessage.printMessage("Répartiteur", portUri,
				"Création du répartiteur de requêtes.");

		// Ajout de l'interface offerte au générateur de requêtes.
		this.addOfferedInterface(RepartiteurRequetesClientI.class);
		RepartiteurRequetesClientInboundPort prrc =
				new RepartiteurRequetesClientInboundPort(portUri + "_Client", this);
		this.addPort(prrc);
		
		if(estDistribue)
			prrc.publishPort();
		else
			prrc.localPublishPort();
		
		// Ajout de l'interface requise pour récupérer les statistiques 
		// d'occupation
		this.addRequiredInterface(ControleurApplicationRepartitionRequeteI.class);
		ControleurApplicationRepartitionRequeteOutboundPort carrop =
				new ControleurApplicationRepartitionRequeteOutboundPort(
						p_portUri + "_CARROP", this);
		this.addPort(carrop);
		carrop.localPublishPort();
		carrop.doConnection(p_uriControleur + "_RepartitionRequete_Inbound",
				ControleurApplicationRepartitionRequeteConnector.class.getCanonicalName());
		controleurApplication = carrop;

		// Ajout de l'interface requise pour l'utilisation des machines virtuelles.
		this.addRequiredInterface(MachineVirtuelleApplicationI.class);
		
		machinesVirtuelles = new HashMap<String, MachineVirtuelleApplicationOutboundPort>();
		
		majMV();
	}




	/* -------------------------------------------------------------- METHODE */
	@Override
	public	void	start
	() throws	ComponentStartException
	{
		super.start();

		ComponentTask rafraichisseur = new ComponentTask() {
			@Override
			public void run() {
				try {
					majMV();
				}
				catch(Exception e) {
					e.printStackTrace();
				}
			}
		};

		// On nettoie la liste a intervalle constant a partir de la fin de la
		// tache precedente, la date de nettoyage dependra de la duree de la
		// tache. Il y a une difference par rapport au calcul du temps moyen,
		// puisqu'on veut reellement marquer une pause entre deux operations.
		super.scheduleTaskWithFixedDelay(rafraichisseur,
				tempsRafraichissement.valeur, tempsRafraichissement.valeur,
				tempsRafraichissement.unite);
	}
	
	
	@Override
	public	void	soumettreRequete
	(
		Requete	p_requete
	) throws	Exception
	{
		synchronized(machinesVirtuelles) {
			Long minStat = Long.MAX_VALUE;
			String uriMVSelected = null;
			boolean nullScore = false;
			
			synchronized(tempsExecution) {
				List<String> selection_uriMV = new ArrayList<>();
				
				for(Entry<String, MachineVirtuelleApplicationOutboundPort> mv : machinesVirtuelles.entrySet()) {
					// 1/ On récupère la liste du score le plus bas :
					//		- null est le score le plus bas
					//		- la moyenne est ensuite utilisée pour établir le score le plus bas
					// 2/ On désigne aléatoirement la MV sur laquelle soumettre une requête
					//		parmis les éléments présents dans la liste du score le plus bas.
					Long currentMVStats = tempsExecution.get(mv.getKey());
					
					// Score le plus bas
					if (currentMVStats == null) {
						// Si le précédent score était "supérieur", alors
						// on écrase la précédente liste
						if (!nullScore) {
							selection_uriMV = new ArrayList<>();
							nullScore = true;
						}
						
						selection_uriMV.add(mv.getKey());
					}
					else if (!nullScore) {
						// Si le précédent score était "supérieur", alors
						// on écrase la précédente liste
						if (currentMVStats <= minStat) {
							if (currentMVStats < minStat) {
								selection_uriMV = new ArrayList<>();
								minStat = currentMVStats;
							}
							selection_uriMV.add(mv.getKey());
						}
					}
				}
				
				// Choisi aléatoirement dans la selection_uriMV
				int nbChoix = selection_uriMV.size();
				Random randomizer = new Random();
				uriMVSelected = selection_uriMV.get(randomizer.nextInt(nbChoix));
			}

			machinesVirtuelles.get(uriMVSelected).soumettreRequete(p_requete);
		}
	}
	
	
	private	void	majMV
	() throws	Exception
	{
		Set<String> suppressionsMV = new HashSet<>();;
		Set<String> ajoutsMV = new HashSet<>();
		
		synchronized(tempsExecution) {
			tempsExecution.clear();
			tempsExecution.putAll(controleurApplication.getMVStats());
			
			synchronized(machinesVirtuelles) {
				for (String uriMV : tempsExecution.keySet()) {
					if (!machinesVirtuelles.containsKey(uriMV)) {
						ajoutsMV.add(uriMV);
					}
				}
				
				for (String uriMV : machinesVirtuelles.keySet()) {
					if (!tempsExecution.containsKey(uriMV)) {
						suppressionsMV.add(uriMV);
					}
				}
				
				for (String uri : ajoutsMV) {
					ajouterMV(uri);
				}
				
				for (String uri : suppressionsMV) {
					retirerMV(uri);
				}
			}
		}
	}
	
	
	private	void	ajouterMV
	(
		String	uriMV
	) throws	Exception
	{
		// Création du port d'utilisation de la machine virtuelle.
		MachineVirtuelleApplicationOutboundPort pmv =
				new MachineVirtuelleApplicationOutboundPort(uriMV + "_Outbound", this);
		this.addPort(pmv);
		
		if(estDistribue)
			pmv.publishPort();
		else
			pmv.localPublishPort();
		
		pmv.doConnection(uriMV + "_Externe", 
				MachineVirtuelleApplicationConnector.class.getCanonicalName());

		synchronized (machinesVirtuelles) {
			machinesVirtuelles.put(uriMV, pmv);
		}
	}
	
	
	private	void	retirerMV
	(
		String	uriMV
	) throws	Exception
	{
		MachineVirtuelleApplicationOutboundPort pmv = null;
		
		synchronized (machinesVirtuelles) {
			pmv = machinesVirtuelles.get(uriMV);
			machinesVirtuelles.remove(uriMV);
		}
		
		pmv.doDisconnection();
	}
}
